package zheezes.socket;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.List;
import java.util.Observable;

import org.apache.log4j.Logger;

import zheezes.protocol.Frame;
import zheezes.protocol.PacketAnalyzer;
import zheezes.util.ArrayUtil;
import zheezes.util.Errno;
import zheezes.util.ExceptReg;
import zheezes.util.Util;


/**
 * @author chen han
 * 
 *         this class is the skeleton, it realized base functions of the
 *         communication, you need to only override little funtions, firstly,
 *         reference to the protocol, e.g. login, logout, because I don't know
 *         about the request and the reponse, senondly, reference to the frame
 *         rewrite, e.g. send
 */
public abstract class Postman extends Observable implements Runnable {
	public static final int MULTI_FRAME_RESPONSE = (1 << 0);
	public static final int CALLBACK_ON_RECEIVE_UNTIL_TIMEOUT = (1 << 1);
	public static final int CALLBACK_ANYHOW_ON_TIMEOUT = (1 << 2);
	public static final int CALLBACK_ONLYWHEN_RECEIVE_ALL = (1 << 3);

	public static final int SOCKED_RD = (1 << 1);
	public static final int SOCKET_WR = (1 << 2);
	public static final int LOGIN     = (1 << 3);
	// public static final int LOGOUT    = (1 << 4);
	
	private Thread cth;
	
	private boolean sleeping = false;
	
	private int interrupt = 1000;

	private static Logger logger = Logger.getLogger(Postman.class);

	protected Socket socket;
	protected InputStream in;
	protected OutputStream out;
	protected int sotime = interrupt; // socket timeout

	private String host;
	private int port;

	private byte[] buffer = new byte[16 * 65536];
	private int length = 0;
	private long sendCount = 0l;
	private long recvCount = 0l;
	
	private PacketAnalyzer packetAnalyzer;

	protected int frameSo = 6 * 1000; // single frame communatie timeout
	protected int carrierSo = 16 * 1000;
	private static final int SESSION_SO = 5 * 60 * 1000; // session timeout
	private Calendar touch = Calendar.getInstance();

	private volatile boolean terminated = false;
	private boolean stopped = false;
	private int state = 0;

	public void run() {
		final Calendar start = Calendar.getInstance();
		cth = Thread.currentThread();
		Frame[] frames = null;
		long nms = 0l;
		long lms = 0l;
		while (!terminated) {
			nms = Calendar.getInstance().getTimeInMillis();
			if ((state & SOCKED_RD) == 0 || (state & SOCKET_WR) == 0) { // if not connected
				/**
				 * when server close the connection
				 * 1. server --> client FIN, server change to FIN_WAIT_1, wait ACK
				 * 2. client --> server ACK, client change to CLOSE_WAIT
				 * 3. server recv ACK, change to FIN_WAIT_2, wait FIN
				 * 
				 * so, the tcp connection is not close normally
				 * so, the clint call close, is send FIN to server and wait ACK
				 * when server recv FIN, will send ACK and change to TIME_WAIT
				 * when client recv ACK, will change to TIME_WAIT
				 * now, the both end is change to TIME_WAIT, close the conection normally
				 */
				state &= ~LOGIN;
				if(connect() == 0) {
					state |= SOCKED_RD;
					state |= SOCKET_WR;
				}
			} else if ((state & LOGIN) == 0) { // if not logined
				if (login() == 0) {
					state |= LOGIN;
					setChanged();
					notifyObservers();
				}
			} else if (nms - touch.getTimeInMillis() >= SESSION_SO) { // if timeout
				if(heartbeat() != 0) {
					state &= ~LOGIN;
				}
			} else { // if connected, logined and not timeout, now, I can handle the frames
				while ((frames = recvFrame()) != null) {
					// handle(frame);
					for (Frame frame : frames) {
						handle(frame);
					}
				}
			}
			if(nms - lms > 60 * 1000) { // every 1 minutes
				lms = nms;
				logger.info(String.format("Postman[%s] send[%d] recv[%d] in %d seconds", desc(), sendCount, recvCount, (nms - start.getTimeInMillis()) / 1000));
			}
			Util.msleep(interrupt);
			if(sleeping) {
				Util.msleep(Integer.MAX_VALUE);
			}
		}
		if ((state & SOCKET_WR) != 0) {
			if ((state & LOGIN) != 0) {
				logout();
				state &= ~LOGIN;
			}
		}
		close();
		stopped = true;
	}

	public void stop() {
		logger.debug(String.format("%s stop", desc()));
		terminated = true;
	}

	private int connect() {
		try {
			socket = new Socket(host, port);
			socket.setSoTimeout(sotime);
			in = socket.getInputStream();
			out = socket.getOutputStream();
		} catch (IOException e) {
			logger.debug(String.format("Postman[%s] connect to %s:%d failure", desc(), host, port));
			setChanged();
			notifyObservers();
			return Errno.EIO;
		}
		return 0;
	}

	/**
	 * after in.close() or out.close(), socket.isClosed() is true
	 * why i write so complex, because
	 * if you close the tcp connection initiative, the tcp change to TIME_WAIT, it's right
	 * but, if is the other peer close the connection, 
	 * as the passive end, it's better to change to CLOSED but not TIME_WAIT
	 * but, if you call the Socket.close(), this peer will change to TIME_WAIT anyway 
	 */
	private void close() {
		if (socket != null) {
			try {
				if ((state & SOCKED_RD) != 0) {
					in.close();
					state &= ~SOCKED_RD;;
				}
				if ((state & SOCKET_WR) != 0) {
					out.close();
					state &= ~SOCKET_WR;
				}
			} catch (IOException e) {
				logger.debug(e.getMessage(), e);
			}
		}
		setChanged();
		notifyObservers();
	}

	protected abstract int login();

	protected abstract int heartbeat();

	protected abstract int logout();

	protected abstract void handle(Frame frame);

	public Transput send(Frame frame) {
		return send(frame, 0);
	}

	public Transput send(Frame frame, int option) {
		if (frame != null && (state & SOCKET_WR) != 0 && (state & LOGIN) != 0) {
			return sendFrame(frame, option);
		}
		return null;
	}

	/* the core of the send */
	protected final int sendFrame(Frame frame) {
		if(frame != null) {
			byte[] buf = frame.getBytes();
			try {
				synchronized (out) {
					out.write(buf);
					touch = Calendar.getInstance();
					sendCount += buf.length;
				}
				logger.info(String.format("Postman[%s] send[%d]:%s", desc(), buf.length, ArrayUtil.sprintf(buf, "%02X ")));
				return 0;
			} catch (IOException e) {
				logger.debug(e.getMessage(), e);
				state &= ~SOCKET_WR;
				close();
			}
		}

		return -1;
	}

	protected abstract Transput sendFrame(Frame frame, int option);

	/**
	 *  the core of the recv
	 *  you never to modify this function with flag state, because it's the core
	 *  function, includes login function will call this when postman was not logined 
	 */
	protected final Frame[] recvFrame() {
		int cnt = 0;
		int len = 0;

		try {
			synchronized (buffer) {
				cnt = in.read(buffer, length, buffer.length - length);
				if (cnt >= 0) {
					length += cnt;
					recvCount += cnt;
					List<Frame> list = new ArrayList<Frame>();
					if ((len = packetAnalyzer.trimPacket(buffer, length)) > 0) {
						logger.info(String.format("Postman[%s] recv[%d]:%s", desc(), len, ArrayUtil.sprintf(buffer, 0, len, "%02X ")));
						Frame frame = packetAnalyzer.parse(buffer, len);
						System.arraycopy(buffer, len, buffer, 0, length - len);
						length -= len;

						if (frame != null) {
							touch = Calendar.getInstance();
							list.add(frame);
							// return frame;
						}
					}
					if(list.size() > 0) {
						return list.toArray(new Frame[0]);
					}
				} else { // the opposing connection is closed, so, it will not to send any data
					logger.info(String.format(desc(), " read end of file, the opposing is closed"));
					state &= ~SOCKED_RD;
					close();
				}
			} // synchronized(buffer)
		} catch (IOException e) {
			int errno = ExceptReg.get(e);
			if (errno == ExceptReg.SOCKET_READ_TIMEOUT) {
			} else {
				logger.debug(e.getMessage(), e);
				state &= ~SOCKED_RD;
				// state &= ~CONNECTED;
				close();
			}
			// break;
		} 
		
		return null;
	}

	/**
	 * you never to call this method manual, this is for link layer check
	 * */
	protected Frame[] recvFrame(int timeout) {
		Calendar end = Calendar.getInstance();
		end.add(Calendar.MILLISECOND, timeout);
		Frame[] frames = null;
		while (((state & SOCKED_RD) != 0) // not must be logined, but must be connected
				&& ((frames = recvFrame()) == null)
				&& (end.compareTo(Calendar.getInstance()) > 0)) {
		}
		return frames;
	}

	public void setHost(String host) {
		this.host = host;
	}

	public void setPort(int port) {
		this.port = port;
	}
	
	public boolean connected() {
		return (state & SOCKED_RD) != 0 && (state & SOCKET_WR) != 0 ? true : false;
	}
	
	public boolean active() {
		return connected() && (state & LOGIN) != 0 ? true : false;
	}

	public boolean live() {
		return !stopped;
	}

	public long getSendCount() {
		return sendCount;
	}

	public long getRecvCount() {
		return recvCount;
	}

	/* most time, it's no necessary to synchronnize, it just a hidden danger */
	public synchronized void setPacketAnalyzer(PacketAnalyzer packetAnalyzer) {
		this.packetAnalyzer = packetAnalyzer;
	}

	public String desc() {
		return String.format("%s:%d", host, port);
	}

	public int getFrameSo() {
		return frameSo;
	}

	public int getCarrierSo() {
		return carrierSo;
	}
	
	public boolean isSleeping() {
		return sleeping;
	}

	public void setSleeping(boolean sleeping) {
		this.sleeping = sleeping;
		cth.interrupt();
	}
}
